<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>复杂的双向绑定原理</title>
</head>
<body>
    <div>
        <span>输入内容</span>
        <input value="" id="input">
    </div>
    <div>
        <span>双向绑定数据</span>
        <p></p>
    </div>
    <button id="submit">提交</button>
    <button id="reset">重置</button>
</body>
<script>
    const Vue = (function() {
        let uid = 0;
        class Dep {
            constructor() {
                this.uid = uid++;
                this.subs = [];
            }

            // 触发target上的Watcher中的addDep方法,参数为dep的实例本身
            depend() {
                Dep.target.addDep(this);
            }

            // 添加订阅者
            addSub(sub) {
                this.subs.push(sub);
            }

            // 通知变化
            notify() {
                this.subs.forEach(sub => sub.update());
            }
        }
        Dep.target = null;

        // 劫持数据
        class Observer {
            constructor(obj) {
                this.value = obj;
                this.walk(obj);
            }

            walk(obj) {
                Object.keys(obj).forEach(key => this.convert(key, obj[key]));
            }
            
            convert(key, val) {
                defineReactive(this.value, key, val);
            }
        }

        function observe(value) {
            if (!value || typeof value !== 'object') {
                return;
            }

            return new Observer(value);
        }

        function defineReactive(obj, key, val) {
            const dep = new Dep();
            let childObj = observe(val);

            Object.defineProperty(obj, key, {
                configurable: true,
                enumerable: true,
                get: function() {
                    if (Dep.target) {
                        dep.depend();
                    }
                    return val;
                },
                set: function(newVal) {
                    if (newVal === val) return;

                    val = newVal;
                    childObj = observe(val);
                    dep.notify();
                }
            });
        }

        // 订阅
        class Watcher {
            constructor(vm, expOrFn, cb) {
                this.depIds = {};
                this.vm = vm;
                this.cb = cb;
                this.expOrFn = expOrFn;
                this.val = this.get();
            }

            get() {
                Dep.target = this;
                const val = this.vm._data[this.expOrFn];
                Dep.target = null;
                return val;
            }

            update() {
                this.run();
            }

            addDep(dep) {
                if (!this.depIds.hasOwnProperty(dep.id)) {
                    dep.addSub(this);
                    this.depIds[dep.id] = dep;
                }
            }

            run() {
                const val = this.get();
                if (val !== this.val) {
                    this.val = val;
                    this.cb.call(this.vm, val);
                }
            }
        }

        class Vue {
            constructor(options) {
                this.$options = options || {};
                this._data = this.$options.data;
                const data = this._data;
                Object.keys(data).forEach(key => this._proxy(key));
                observe(data);
            }

            $watch(expOrFn, cb) {
                new Watcher(this, expOrFn, cb);
            }

            _proxy(key) {
                Object.defineProperty(this, key, {
                    configurable: true,
                    enumerable: true,
                    get: () => this._data[key],
                    set: val => {
                        this._data[key] = val;
                    },
                });
            }
        }

        return Vue;
    }());
    
    (function() {
        let demo = new Vue({
            data: {
                val: ''
            }
        });

        const inputElem = document.querySelector('#input');
        const submitElem = document.querySelector('#submit');
        const resetElem = document.querySelector('#reset');

        inputElem.addEventListener('keyup', function(e) {
            demo.val = e.target.value;
        });

        submitElem.addEventListener('click', function() {
            let val = demo.val;
            alert(val);
        });

        resetElem.addEventListener('click', function() {
            demo.val = '';
        });

        demo.$watch('val', function(str) {
            document.querySelector('p').innerText = str;
            document.querySelector('#input').value = str;
        });
    }());
</script>
</html>